/*
 * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.apple.laf;

import java.awt.*;
import java.awt.event.*;
import java.beans.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.TextUI;
import javax.swing.text.JTextComponent;

import apple.laf.JRSUIConstants.*;

import com.apple.laf.AquaIcon.DynamicallySizingJRSUIIcon;
import com.apple.laf.AquaUtilControlSize.*;
import com.apple.laf.AquaUtils.*;

public final class AquaTextFieldSearch {
    private static final String VARIANT_KEY = "JTextField.variant";
    private static final String SEARCH_VARIANT_VALUE = "search";

    private static final String FIND_POPUP_KEY = "JTextField.Search.FindPopup";
    private static final String FIND_ACTION_KEY = "JTextField.Search.FindAction";
    private static final String CANCEL_ACTION_KEY = "JTextField.Search.CancelAction";
    private static final String PROMPT_KEY = "JTextField.Search.Prompt";

    private static final SearchFieldPropertyListener SEARCH_FIELD_PROPERTY_LISTENER = new SearchFieldPropertyListener();
    protected static void installSearchFieldListener(final JTextComponent c) {
        c.addPropertyChangeListener(SEARCH_FIELD_PROPERTY_LISTENER);
    }

    protected static void uninstallSearchFieldListener(final JTextComponent c) {
        c.removePropertyChangeListener(SEARCH_FIELD_PROPERTY_LISTENER);
    }

    static final class SearchFieldPropertyListener implements PropertyChangeListener {
        @Override
        public void propertyChange(final PropertyChangeEvent evt) {
            final Object source = evt.getSource();
            if (!(source instanceof JTextComponent)) return;

            final String propertyName = evt.getPropertyName();
            if (!VARIANT_KEY.equals(propertyName) &&
                !FIND_POPUP_KEY.equals(propertyName) &&
                !FIND_ACTION_KEY.equals(propertyName) &&
                !CANCEL_ACTION_KEY.equals(propertyName) &&
                !PROMPT_KEY.equals(propertyName)) {
                return;
            }

            final JTextComponent c = (JTextComponent)source;
            if (wantsToBeASearchField(c)) {
                uninstallSearchField(c);
                installSearchField(c);
            } else {
                uninstallSearchField(c);
            }
        }
    }

    protected static boolean wantsToBeASearchField(final JTextComponent c) {
        return SEARCH_VARIANT_VALUE.equals(c.getClientProperty(VARIANT_KEY));
    }

    protected static boolean hasPopupMenu(final JTextComponent c) {
        return (c.getClientProperty(FIND_POPUP_KEY) instanceof JPopupMenu);
    }

    private static final RecyclableSingleton<SearchFieldBorder> instance = new RecyclableSingletonFromDefaultConstructor<SearchFieldBorder>(SearchFieldBorder.class);
    public static SearchFieldBorder getSearchTextFieldBorder() {
        return instance.get();
    }

    protected static void installSearchField(final JTextComponent c) {
        final SearchFieldBorder border = getSearchTextFieldBorder();
        c.setBorder(border);
        c.setLayout(border.getCustomLayout());
        c.add(getFindButton(c), BorderLayout.WEST);
        c.add(getCancelButton(c), BorderLayout.EAST);
        c.add(getPromptLabel(c), BorderLayout.CENTER);

        final TextUI ui = c.getUI();
        if (ui instanceof AquaTextFieldUI) {
            ((AquaTextFieldUI)ui).setPaintingDelegate(border);
        }
    }

    protected static void uninstallSearchField(final JTextComponent c) {
        c.setBorder(UIManager.getBorder("TextField.border"));
        c.removeAll();

        final TextUI ui = c.getUI();
        if (ui instanceof AquaTextFieldUI) {
            ((AquaTextFieldUI)ui).setPaintingDelegate(null);
        }
    }

    // The "magnifying glass" icon that sometimes has a downward pointing triangle next to it
    // if a popup has been assigned to it. It does not appear to have a pressed state.
    protected static DynamicallySizingJRSUIIcon getFindIcon(final JTextComponent text) {
        return (text.getClientProperty(FIND_POPUP_KEY) == null) ?
            new DynamicallySizingJRSUIIcon(new SizeDescriptor(new SizeVariant(25, 22).alterMargins(0, 4, 0, -5))) {
                public void initJRSUIState() {
                    painter.state.set(Widget.BUTTON_SEARCH_FIELD_FIND);
                }
            }
        :
            new DynamicallySizingJRSUIIcon(new SizeDescriptor(new SizeVariant(25, 22).alterMargins(0, 4, 0, 2))) {
                public void initJRSUIState() {
                    painter.state.set(Widget.BUTTON_SEARCH_FIELD_FIND);
                }
            }
        ;
    }

    // The "X in a circle" that only shows up when there is text in the search field.
    protected static DynamicallySizingJRSUIIcon getCancelIcon() {
        return new DynamicallySizingJRSUIIcon(new SizeDescriptor(new SizeVariant(22, 22).alterMargins(0, 0, 0, 4))) {
            public void initJRSUIState() {
                painter.state.set(Widget.BUTTON_SEARCH_FIELD_CANCEL);
            }
        };
    }

    protected static State getState(final JButton b) {
        if (!AquaFocusHandler.isActive(b)) return State.INACTIVE;
        if (b.getModel().isPressed()) return State.PRESSED;
        return State.ACTIVE;
    }

    protected static JButton createButton(final JTextComponent c, final DynamicallySizingJRSUIIcon icon) {
        final JButton b = new JButton()
//        {
//            public void paint(Graphics g) {
//                super.paint(g);
//
//                g.setColor(Color.green);
//                g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
//            }
//        }
        ;

        final Insets i = icon.sizeVariant.margins;
        b.setBorder(BorderFactory.createEmptyBorder(i.top, i.left, i.bottom, i.right));

        b.setIcon(icon);
        b.setBorderPainted(false);
        b.setFocusable(false);
        b.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
        b.addChangeListener(new ChangeListener() {
            public void stateChanged(final ChangeEvent e) {
                icon.painter.state.set(getState(b));
            }
        });
        b.addMouseListener(new MouseAdapter() {
            public void mousePressed(final MouseEvent e) {
                c.requestFocusInWindow(FocusEvent.Cause.MOUSE_EVENT);
            }
        });

        return b;
    }

    protected static JButton getFindButton(final JTextComponent c) {
        final DynamicallySizingJRSUIIcon findIcon = getFindIcon(c);
        final JButton b = createButton(c, findIcon);
        b.setName("find");

        final Object findPopup = c.getClientProperty(FIND_POPUP_KEY);
        if (findPopup instanceof JPopupMenu) {
            // if we have a popup, indicate that in the icon
            findIcon.painter.state.set(Variant.MENU_GLYPH);

            b.addMouseListener(new MouseAdapter() {
                public void mousePressed(final MouseEvent e) {
                    ((JPopupMenu)findPopup).show(b, 8, b.getHeight() - 2);
                    c.requestFocusInWindow(FocusEvent.Cause.MOUSE_EVENT);
                    c.repaint();
                }
            });
        }

        final Object findAction = c.getClientProperty(FIND_ACTION_KEY);
        if (findAction instanceof ActionListener) {
            b.addActionListener((ActionListener)findAction);
        }

        return b;
    }

    private static Component getPromptLabel(final JTextComponent c) {
        final JLabel label = new JLabel();
        label.setForeground(UIManager.getColor("TextField.inactiveForeground"));

        c.getDocument().addDocumentListener(new DocumentListener() {
            public void changedUpdate(final DocumentEvent e) { updatePromptLabel(label, c); }
            public void insertUpdate(final DocumentEvent e) { updatePromptLabel(label, c); }
            public void removeUpdate(final DocumentEvent e) { updatePromptLabel(label, c); }
        });
        c.addFocusListener(new FocusAdapter() {
            public void focusGained(final FocusEvent e) { updatePromptLabel(label, c); }
            public void focusLost(final FocusEvent e) { updatePromptLabel(label, c); }
        });
        updatePromptLabel(label, c);

        return label;
    }

    static void updatePromptLabel(final JLabel label, final JTextComponent text) {
        if (SwingUtilities.isEventDispatchThread()) {
            updatePromptLabelOnEDT(label, text);
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() { updatePromptLabelOnEDT(label, text); }
            });
        }
    }

    static void updatePromptLabelOnEDT(final JLabel label, final JTextComponent text) {
        String promptText = " ";
        if (!text.hasFocus() && "".equals(text.getText())) {
            final Object prompt = text.getClientProperty(PROMPT_KEY);
            if (prompt != null) promptText = prompt.toString();
        }
        label.setText(promptText);
    }

    protected static JButton getCancelButton(final JTextComponent c) {
        final JButton b = createButton(c, getCancelIcon());
        b.setName("cancel");

        final Object cancelAction = c.getClientProperty(CANCEL_ACTION_KEY);
        if (cancelAction instanceof ActionListener) {
            b.addActionListener((ActionListener)cancelAction);
        }

        b.addActionListener(new AbstractAction("cancel") {
            public void actionPerformed(final ActionEvent e) {
                c.setText("");
            }
        });

        c.getDocument().addDocumentListener(new DocumentListener() {
            public void changedUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
            public void insertUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
            public void removeUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
        });

        updateCancelIcon(b, c);
        return b;
    }

    // <rdar://problem/6444328> JTextField.variant=search: not thread-safe
    static void updateCancelIcon(final JButton button, final JTextComponent text) {
        if (SwingUtilities.isEventDispatchThread()) {
            updateCancelIconOnEDT(button, text);
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() { updateCancelIconOnEDT(button, text); }
            });
        }
    }

    static void updateCancelIconOnEDT(final JButton button, final JTextComponent text) {
        button.setVisible(!"".equals(text.getText()));
    }

    // subclass of normal text border, because we still want all the normal text field behaviors
    static final class SearchFieldBorder extends AquaTextFieldBorder implements JComponentPainter {
        protected boolean reallyPaintBorder;

        public SearchFieldBorder() {
            super(new SizeDescriptor(new SizeVariant().alterMargins(6, 31, 6, 24).alterInsets(3, 3, 3, 3)));
            painter.state.set(Widget.FRAME_TEXT_FIELD_ROUND);
        }

        public SearchFieldBorder(final SearchFieldBorder other) {
            super(other);
        }

        @Override
        public void paint(final JComponent c, final Graphics g, final int x, final int y, final int w, final int h) {
            reallyPaintBorder = true;
            paintBorder(c, g, x, y, w, h);
            reallyPaintBorder = false;
        }

        // apparently without adjusting for odd height pixels, the search field "wobbles" relative to it's contents
        @Override
        public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
            if (!reallyPaintBorder) return;
            super.paintBorder(c, g, x, y - (height % 2), width, height);
        }

        @Override
        public Insets getBorderInsets(final Component c) {
            if (doingLayout) return new Insets(0, 0, 0, 0);

            if (!hasPopupMenu((JTextComponent)c)) {
                return new Insets(sizeVariant.margins.top, sizeVariant.margins.left - 7, sizeVariant.margins.bottom, sizeVariant.margins.right);
            }

            return sizeVariant.margins;
        }

        protected boolean doingLayout;
        protected LayoutManager getCustomLayout() {
            // unfortunately, the default behavior of BorderLayout, which accommodates for margins
            // is not what we want, so we "turn off margins" for layout for layout out our buttons
            return new BorderLayout(0, 0) {
                public void layoutContainer(final Container target) {
                    doingLayout = true;
                    super.layoutContainer(target);
                    doingLayout = false;
                }
            };
        }
    }
}
